﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.OLE.Interop;
using System.Windows.Forms;
using System.Diagnostics.CodeAnalysis;
#if NET40
using Microsoft.Build.Evaluation;
#endif
using System.ComponentModel;
using System.Drawing;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio;
using System.Runtime.InteropServices;
#if !NET40
using Microsoft.Build.BuildEngine;
#endif

namespace NTrace.VSPackage
{
    [System.Runtime.InteropServices.ComVisible(true)]
    public abstract class PropertyPageBase : IPropertyPage, IDisposable
    {
        private System.IServiceProvider serviceProvider;
        private Microsoft.VisualStudio.Shell.Interop.IVsHierarchy innerVsHierarchy;
        private Guid projectId;
#if NET40
        private ProjectCollection buildEngine;
#else
        private Microsoft.Build.BuildEngine.Engine buildEngine;
#endif
        private Project buildProject;
        private String name;
        private Panel panel;
        private bool active;
        private bool dirty;
        public Size Size { get; set; }
        private IPropertyPageSite site;
        private bool isDisposed;
        private static volatile object SynchronizationObject = new object();

        public String Name
        {
            get;
            protected set;
        }

        public bool IsDirty
        {
            get
            {
                return this.dirty;
            }
            protected set
            {
                if (this.dirty != value)
                {
                    this.dirty = value;
                    if (this.site != null)
                        site.OnStatusChange((uint)(this.dirty ? PropPageStatus.Dirty : PropPageStatus.Clean));
                    this.SetProjectFileDirty();
                }
            }
        }

        public Project BuildProject
        {
            get
            {
                return this.buildProject;
            }
            private set
            {
                this.buildProject = value;
            }
        }

        protected UserControl EditingSurface
        {
            get;
            private set;
        }

        /// <summary>
        /// This is the project instance guid that is peristed in the project file
        /// </summary>
        [System.ComponentModel.BrowsableAttribute(false)]
        [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ID")]
        public Guid ProjectID
        {
            get
            {
                return this.projectId;
            }
        }

        private String ProjectPath
        {
            get
            {
                System.Diagnostics.Debug.Assert(this.BuildProject != null);
                String projectPath;
#if NET40
                projectPath = this.BuildProject.FullPath;
#else
                projectPath = this.BuildProject.FullFileName;
#endif
                return projectPath;
            }
        }

        public virtual void SetProjectFileDirty()
        {
#if NET40
            this.buildProject.MarkDirty();
#else
            this.buildProject.MarkProjectAsDirty();
#endif
        }

        #region IPropertyPage Members

        /// <summary>
        /// Creates the dialog box window for the property page.
        /// </summary>
        /// <param name="hWndParent">
        /// The window handle of the parent of the dialog box that is being created.
        /// </param>
        /// <param name="pRect">
        /// A pointer to the RECT structure containing the positioning information for the dialog box. This method must create its dialog box with the placement and dimensions described by this structure.
        /// </param>
        /// <param name="bModal">
        /// Indicates whether the dialog box frame is modal (TRUE) or modeless (FALSE).
        /// </param>
        public void Activate(IntPtr hWndParent, Microsoft.VisualStudio.OLE.Interop.RECT[] pRect, int bModal)
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::Activate");

            if (pRect != null && pRect.Length > 0)
            {
                this.Size = new Size(pRect[0].right - pRect[0].left, pRect[0].bottom - pRect[0].top);
            }

            if (this.panel == null)
            {
                if (pRect == null)
                {
                    throw new ArgumentNullException("pRect");
                }

                this.panel = new Panel();
                this.panel.Size = this.Size;
                this.panel.Text = Strings.Settings;
                this.panel.Visible = false;
                this.panel.CreateControl();
                try
                {
                    this.OnReadConfiguration();

                    // Have the sub-class set up the property page's surface
                    this.EditingSurface = this.CreatePropertyPageControl();
                    this.panel.Controls.Add(this.EditingSurface);
                }
                catch (Exception e)
                {
                    EtwTrace.Trace("Unable to create hosted property page: {0}", e);
                }
                NativeMethods.SetParent(this.panel.Handle, hWndParent);
            }
        }

        /// <summary>
        /// Set value of project property
        /// </summary>
        /// <param name="propertyName">Name of property</param>
        /// <param name="propertyValue">Value of property</param>
        protected void SetProperty(string propertyName, string propertyValue)
        {
            if (propertyName == null)
                throw new ArgumentNullException("propertyName", "Cannot set a null project property");

            string oldValue = this.GetProperty(propertyName);
            if (propertyValue == null)
            {
                // if property already null, do nothing
                if (oldValue == null)
                    return;
                // otherwise, set it to empty
                propertyValue = String.Empty;
            }

            // Only do the work if this is different to what we had before
            if (String.Compare(oldValue, propertyValue, StringComparison.Ordinal) != 0)
            {
                // Check out the project file.
                if (!this.QueryEditProjectFile(false))
                {
                    throw Marshal.GetExceptionForHR(VSConstants.OLE_E_PROMPTSAVECANCELLED);
                }

                this.BuildProject.SetProperty(propertyName, propertyValue);
            }
            return;
        }

        /// <summary>
        /// Applies the current values to the underlying objects associated with
        /// the property page as previously passed to <see cref="SetObjects"/>
        /// </summary>
        /// <returns>
        /// S_OK: Changes were successfully applied and the property page is current with the underlying objects.
        /// S_FALSE: Changes were applied, but the property page cannot determine if its state is current with the objects.
        /// </returns>
        public int Apply()
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::Apply");
            if (this.IsDirty)
            {
                this.OnWriteConfiguration();
                this.IsDirty = false;
            }

            return HResult.S_OK;
        }

        /// <summary>
        /// Destroys the window created in <see cref="Activate"/>.
        /// </summary>
        public void Deactivate()
        {
            if (this.panel != null)
            {
                this.panel.Dispose();
                this.panel = null;
            }
        }

        /// <summary>
        /// Returns a stucture describing this property page.
        /// </summary>
        /// <param name="pPageInfo">
        /// A pointer to the caller-allocated PROPPAGEINFO structure in which 
        /// the property page stores its page information. All allocations
        /// stored in this structure become the responsibility of the caller.
        /// </param>
        public void GetPageInfo(Microsoft.VisualStudio.OLE.Interop.PROPPAGEINFO[] pPageInfo)
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::GetPageInfo");
            if (pPageInfo != null && pPageInfo.Length > 0)
            {
                PROPPAGEINFO info = new PROPPAGEINFO();
                info.cb = (uint)Marshal.SizeOf(typeof(PROPPAGEINFO));
                info.dwHelpContext = 0;
                info.pszDocString = null;
                info.pszHelpFile = null;
                info.pszTitle = this.Name;
                info.SIZE.cx = this.Size.Width;
                info.SIZE.cy = this.Size.Height;
                pPageInfo[0] = info;
            }
        }

        /// <summary>
        /// Invokes the property page help in response to an end-user request.
        /// </summary>
        /// <param name="pszHelpDir">
        /// A string containingin the a path to the property page's help.
        /// </param>
        /// <remarks>
        /// This is never called from an project property page.
        /// </remarks>
        public void Help(string pszHelpDir)
        {
            EtwTrace.Trace(EtwTraceLevel.Warning, EtwTraceFlag.StackTracing, "NTracePropertyPage::Help - this should never be called from a property page!?!");
        }

        /// <summary>
        /// Indicates whether the property page has changed since it was activated or since the most recent call to Apply.
        /// </summary>
        /// <returns>
        /// This method returns S_OK to indicate that the property page has changed. Otherwise, it returns S_FALSE.
        /// </returns>
        public int IsPageDirty()
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::IsPageDirty");
            int result = this.dirty ? HResult.S_OK : HResult.S_FALSE;
            EtwTrace.Trace(EtwTraceLevel.Information, EtwTraceFlag.StackTracing, "NTracePropertyPage::IsPageDirty - returning {0}", HResult.ToString(result));
            return result;
        }

        public void Move(Microsoft.VisualStudio.OLE.Interop.RECT[] pRect)
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::Move");
        }

        public void SetObjects(uint count, object[] ppunk)
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::SetObjects");
            if (ppunk == null)
            {
                return;
            }

            if (count > 0)
            {
                // Cast the IUnknown to a IVsBrowseObject; this is so we can obtain the project item,
                // which we need so that we can call GetSite
                Microsoft.VisualStudio.Shell.Interop.IVsBrowseObject browseObject = ppunk[0] as Microsoft.VisualStudio.Shell.Interop.IVsBrowseObject;
                uint itemId;
                browseObject.GetProjectItem(out this.innerVsHierarchy, out itemId);
                this.serviceProvider = new ServiceProvider(this.GetSite());

                // This is almost a No op if the engine has already been instantiated in the factory.
                this.buildEngine = this.InitializeMsBuildEngine(this.buildEngine);

                this.SetBuildProject();

            }
        }

        public void SetPageSite(Microsoft.VisualStudio.OLE.Interop.IPropertyPageSite pPageSite)
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::SetPageSite");
            this.site = pPageSite;
        }

        public void Show(uint nCmdShow)
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::Show - nCmdShow = {0}", nCmdShow);
            switch (nCmdShow)
            {
                case 1: //SW_SHOWNORMAL
                case 5: //SW_SHOW
                    this.panel.Visible = true;
                    break;
                default:
                    throw new ArgumentException();
            }
        }

        public int TranslateAccelerator(Microsoft.VisualStudio.OLE.Interop.MSG[] pMsg)
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::TranslateAccelerator");
            throw new NotImplementedException();
        }

        #endregion

        #region IDisposable Members
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        private void Dispose(bool disposing)
        {
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.StackTracing, "NTracePropertyPage::Dispose");
            if (!this.isDisposed)
            {
                lock (NTracePropertyPage.SynchronizationObject)
                {
                    if (disposing)
                    {
                        this.panel.Dispose();
                    }

                    this.isDisposed = true;
                }
            }
        }

        protected virtual void OnReadConfiguration()
        {
        }

        protected virtual void OnWriteConfiguration()
        {
        }

#if NET40
        /// <summary>
        /// Initialize the build engine. Sets the build enabled property to true. The engine is initialzed if the passed in engine is null or does not have its bin path set.
        /// </summary>
        /// <param name="engine">An instance of MSBuild.ProjectCollection build engine, that will be checked if initialized.</param>
        /// <param name="engine">The service provider.</param>
        /// <returns>The buildengine to use.</returns>
        private ProjectCollection InitializeMsBuildEngine(ProjectCollection existingEngine)
        {
            //if (this.serviceProvider == null)
            //{
            //    throw new ArgumentNullException("serviceProvider");
            //}

            if (existingEngine == null)
            {
                ProjectCollection buildEngine = ProjectCollection.GlobalProjectCollection;
                return buildEngine;
            }

            return existingEngine;
        }
#else
        /// <summary>
        /// Initialize the build engine. Sets the build enabled property to true. The engine is initialzed if the passed in engine is null or does not have its bin path set.
        /// </summary>
        /// <param name="engine">An instance of <see cref="Microsoft.Build.BuildEngine.Engine"/>, that will be checked if initialized.</param>
        /// <param name="engine">The service provider.</param>
        /// <returns>The buildengine to use.</returns>
        private Microsoft.Build.BuildEngine.Engine InitializeMsBuildEngine(Microsoft.Build.BuildEngine.Engine existingEngine)
        {
            //if (this.serviceProvider == null)
            //{
            //    throw new ArgumentNullException("serviceProvider");
            //}

            if (existingEngine == null)
            {
                Microsoft.Build.BuildEngine.Engine buildEngine = Microsoft.Build.BuildEngine.Engine.GlobalEngine;
                return buildEngine;
            }

            return existingEngine;
        }
#endif
        public T GetProperty<T>(string name, Type type)
        {
            String value = this.GetProperty(name);
            if (String.IsNullOrEmpty(value)) return default(T);

            TypeConverter tc = TypeDescriptor.GetConverter(typeof(T));
            return (T)tc.ConvertFromInvariantString(value);
        }

        public String GetProperty(String propertyName)
        {
            return this.GetProperty(this.BuildProject, propertyName);
        }

        private String GetProperty(Project project, String propertyName)
        {
            String output = null;
            if (project != null)
            {
#if NET40
                ProjectProperty property = project.GetProperty(propertyName);

                if (property != null)
                {
                    output = property.EvaluatedValue;
                }
#else
                BuildProperty property = project.EvaluatedProperties[propertyName];

                if (property != null)
                {
                    output = property.Value;
                }
#endif
            }

            return output;
        }

        private Microsoft.VisualStudio.OLE.Interop.IServiceProvider GetSite()
        {
            Microsoft.VisualStudio.OLE.Interop.IServiceProvider serviceProvider;

            System.Diagnostics.Debug.Assert(this.innerVsHierarchy != null, "Expected a non-null IVsHierarchy object.");
            ErrorHandler.ThrowOnFailure(this.innerVsHierarchy.GetSite(out serviceProvider));

            return serviceProvider;
        }

        /// <summary>
        /// Verify if the file can be written to.
        /// Return false if the file is read only and/or not checked out
        /// and the user did not give permission to change it.
        /// Note that exact behavior can also be affected based on the SCC
        /// settings under Tools->Options.
        /// </summary>
        private bool QueryEditProjectFile(bool suppressUI)
        {
            bool result = true;
            if (this.GetSite() == null)
            {
                // We're already zombied. Better return FALSE.
                result = false;
            }
            else
            {
                IVsQueryEditQuerySave2 queryEditQuerySave = this.serviceProvider.GetService(typeof(SVsQueryEditQuerySave)) as IVsQueryEditQuerySave2;
                if (queryEditQuerySave != null)
                {
                    tagVSQueryEditFlags qef = tagVSQueryEditFlags.QEF_AllowInMemoryEdits;
                    if (suppressUI)
                        qef |= tagVSQueryEditFlags.QEF_SilentMode;

                    // If we are debugging, we want to prevent our project from being reloaded. To 
                    // do this, we pass the QEF_NoReload flag
                    if (!Utilities.IsVisualStudioInDesignMode(this.serviceProvider))
                        qef |= tagVSQueryEditFlags.QEF_NoReload;

                    uint verdict;
                    uint moreInfo;
                    string[] files = new string[1];
                    files[0] = this.ProjectPath;
                    uint[] flags = new uint[1];
                    VSQEQS_FILE_ATTRIBUTE_DATA[] attributes = new VSQEQS_FILE_ATTRIBUTE_DATA[1];
                    int hr = queryEditQuerySave.QueryEditFiles(
                                    (uint)qef,
                                    1, // 1 file
                                    files, // array of files
                                    flags, // no per file flags
                                    attributes, // no per file file attributes
                                    out verdict,
                                    out moreInfo /* ignore additional results */);

                    tagVSQueryEditResult qer = (tagVSQueryEditResult)verdict;
                    if (ErrorHandler.Failed(hr) || (qer != tagVSQueryEditResult.QER_EditOK))
                    {
                        if (!suppressUI && !Utilities.IsInAutomationFunction(this.serviceProvider))
                        {
                            string message = String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.CancelQueryEdit, this.ProjectPath);
                            string title = string.Empty;
                            OLEMSGICON icon = OLEMSGICON.OLEMSGICON_CRITICAL;
                            OLEMSGBUTTON buttons = OLEMSGBUTTON.OLEMSGBUTTON_OK;
                            OLEMSGDEFBUTTON defaultButton = OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST;
                            VsShellUtilities.ShowMessageBox(this.serviceProvider, title, message, icon, buttons, defaultButton);
                        }
                        result = false;
                    }
                }
            }
            return result;
        }

        private void SetBuildProject()
        {
            int hr = this.innerVsHierarchy.GetGuidProperty(
                VSConstants.VSITEMID_ROOT,
                (int)__VSHPROPID.VSHPROPID_ProjectIDGuid,
                out this.projectId);
            if (!ErrorHandler.Succeeded(hr))
            {
                EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.Diagnostic, "IVsHierarchy::GetGuidProperty returned {0}", hr);
            }
            ErrorHandler.ThrowOnFailure(hr);

#if NET40
            this.BuildProject = (from project in this.buildEngine.LoadedProjects
                                 where this.IsCurrentProject(project)
                                 select project).FirstOrDefault();
#else
            object unknown;
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.Diagnostic, "Retrieving a handle to the root object...", hr);
            hr = this.innerVsHierarchy.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_ExtObject, out unknown);
            if (!ErrorHandler.Succeeded(hr))
            {
                EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.Diagnostic, "IVsHierarchy::GetProperty returned {0}", hr);
            }
            ErrorHandler.ThrowOnFailure(hr);

            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.Diagnostic, "Converting to an EnvDTE.Project...", hr);
            EnvDTE.Project project = unknown as EnvDTE.Project;
            System.Diagnostics.Debug.Assert(project != null);
            EtwTrace.Trace(EtwTraceLevel.Verbose, EtwTraceFlag.Diagnostic, "Retrieving project '{0}'", project.FullName, hr);
            this.BuildProject = this.buildEngine.GetLoadedProject(project.FullName);
#endif
            System.Diagnostics.Debug.Assert(this.BuildProject != null);
            if (this.BuildProject == null)
            {
                EtwTrace.Trace(EtwTraceLevel.Error, EtwTraceFlag.Diagnostic, "Unable to load the current project from MSBuild engine.");
                throw new InvalidOperationException(Strings.CantLoadMSBuildProject);
            }
        }

        private bool IsCurrentProject(Project project)
        {
            bool output = false;
            String projectGuid = this.GetProperty(project, "ProjectGuid");
            if (!String.IsNullOrEmpty(projectGuid))
            {
                try
                {
                    Guid id = new Guid(projectGuid);
                    output = this.projectId == id;
                }
                catch (FormatException)
                {
                }
            }
            return output;
        }

        protected abstract UserControl CreatePropertyPageControl();
    }
}
